# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: xguo

import math
import json
from typing import List

import coin.flow.subscriber as flow_subscriber
import coin.proto.coin_market_enums_pb2 as coin_enum

from coin.base.param_util import to_list
from coin.base.config import Config
from coin.base.timestamp import get_timestamp
from coin.exchange.fcoin.kr_rest.native_private_client import FcoinNativePrivateClient
from coin.exchange.fcoin.kr_rest.product import FcoinProduct, FcoinCurrency
from coin.exchange.fcoin.rest_queryer import FcoinRestQueryer
from coin.proto.coin_order_gateway_pb2 import OrderGatewayConfig
from coin.exchange.base.order_fill_util import FillManager

from coin.exchange.base.rest_order_gateway_base import (
    RestOrderGateway,
    exchange_message_handler,
    create_order_internal,
)
from coin.proto.coin_query_pb2 import (
    CurrencyBalance,
    AccountBalance,
)
from coin.exchange.base.order_gateway import (
    BaseOrderGatewayInfoMutator,
    Order,
    OrderSide,
    OrderType,
    OrderGatewayStatus,
)


def create_order_from_order_info(order_id, order_info):
  side_map = {
      'buy': OrderSide.BUY,
      'sell': OrderSide.SELL,
  }
  type_map = {
      'limit': OrderType.LIMIT,
      'market': OrderType.MARKET,
  }
  price_orig = float(order_info['price'])
  qty_orig = float(order_info['amount'])
  qty_accum_fill = float(order_info['filled_amount'])
  if math.isclose(qty_accum_fill, 0):
    price_avg_fill = None
  else:
    price_avg_fill = float(order_info['executed_value']) / qty_accum_fill
  order_side = side_map[order_info['side']]
  order_type = type_map[order_info['type']]
  exchange_order_id = order_info['id']
  product = FcoinProduct.FromStr(order_info['symbol'])
  fill = FillManager(
      qty_orig=qty_orig,
      price_orig=price_orig,
      qty_accum_fill=qty_accum_fill,
      price_avg_fill=price_avg_fill,
      qty_last_fill=qty_accum_fill,
      price_last_fill=price_avg_fill,
  )
  order = Order(order_id=order_id,
                product=product,
                price=price_orig,
                qty=qty_orig,
                order_side=order_side,
                order_type=order_type,
                tag=None,
                accepted=True,
                cancel_sent=False,
                internal=create_order_internal(
                    timestamp=get_timestamp(),
                    exchange_order_id=exchange_order_id,
                    fill=fill,
                ))
  return order


class FcoinOrderGatewayInfo(BaseOrderGatewayInfoMutator):
  CurrencyType = FcoinCurrency
  ProductType = FcoinProduct

  def update_balance(self, update_msg):
    assert update_msg['status'] == 0, update_msg
    for balance_info in update_msg['data']:
      currency = FcoinCurrency.FromStrNativeCurrencyNothrow(balance_info['currency'])
      if currency is None:
        continue
      balance = float(balance_info['balance'])
      available = float(balance_info['available'])
      hold = float(balance_info['frozen'])
      currency_balance = CurrencyBalance(currency=currency.currency,
                                         currency_native=currency.native_currency,
                                         total=balance,
                                         available=available,
                                         hold=hold)
      self.set_balance(currency, balance, currency_balance)

  def balance_as_proto(self):
    return AccountBalance(exchange='Fcoin',
                          market_type='Spot',
                          each_balance=self.get_notNone_balances_internal())


class FcoinOrderGateway(RestOrderGateway):
  _exchange = 'Fcoin'
  _market_type = 'Spot'
  _api_version = None

  OGInfoType = FcoinOrderGatewayInfo

  def __init__(self,
               currencies: List[FcoinCurrency],
               products: List[FcoinProduct],
               config: Config,
               og_config=None,
               logger=None):
    super().__init__(currencies, products, config, og_config, logger)

    self._private_client = None
    self._rest_queryer = None

    self._ioloop = None
    self._flow_sub = None

    self._og_config = OrderGatewayConfig(account_query_period=1,
                                         orders_snapshot_query_period=1,
                                         dead_orders_query_period=1)
    self._og_logger.exchange_type = coin_enum.Fcoin

  def start(self, queue, ioloop, flow_sub=None):
    self._private_client = FcoinNativePrivateClient(key_file=self._config.key_file, sync=False)
    self._ioloop = ioloop
    self._rest_queryer = FcoinRestQueryer(queue=queue, worker_id='1', ioloop=ioloop)
    self._install_periodic_query()

    # Subscribe to flow for feed.
    self._flow_sub = flow_sub or flow_subscriber.from_queue(queue)
    self._flow_sub.subscribe('fcoin_rest_private_1', self.on_private_exchange_msg)

  def _install_periodic_query(self):
    assert self._rest_queryer is not None, 'self._rest_query is not set!'

    # query balance
    self._rest_queryer.add_periodic_query(self._private_client.query_GET_accounts_balance,
                                          job_name='query_account_balance',
                                          msg_type='account_balance',
                                          query_period=self._og_config.account_query_period)

    # query working orders
    for product in self._og_info.products:
      self._rest_queryer.add_periodic_query(
          self._private_client.query_GET_orders,
          job_name='query_order_info_submitted',
          msg_type='order_info',
          symbol=product.native_symbol,
          states='submitted',
          query_period=self._og_config.orders_snapshot_query_period)

      self._rest_queryer.add_periodic_query(
          self._private_client.query_GET_orders,
          job_name='query_order_info_partial_filled',
          msg_type='order_info',
          symbol=product.native_symbol,
          states='partial_filled',
          query_period=self._og_config.orders_snapshot_query_period)

      self._rest_queryer.add_periodic_query(self._private_client.query_GET_orders,
                                            job_name='query_order_info_filled',
                                            msg_type='order_info',
                                            symbol=product.native_symbol,
                                            states='filled',
                                            query_period=self._og_config.dead_orders_query_period)

      self._rest_queryer.add_periodic_query(self._private_client.query_GET_orders,
                                            job_name='query_order_info_canceled',
                                            msg_type='order_info',
                                            symbol=product.native_symbol,
                                            states='canceled',
                                            query_period=self._og_config.dead_orders_query_period)

    self._rest_queryer.start()

  def _async_submit_order(self, order, post_only=False):
    if post_only:
      self._logger.warning('post only not supported.')
    assert order.order_id is not None, order
    assert order.order_type == OrderType.LIMIT, order

    side_map = {
        OrderSide.BUY: 'buy',
        OrderSide.SELL: 'sell',
    }
    formatter = self._order_formatter[order.product]
    params = {
        'symbol': order.product.native_symbol,
        'order_side': side_map[order.order_side],
        'order_type': 'limit',
        'price': formatter.format_price(order.price),
        'amount': formatter.format_qty(order.qty),
    }
    fut = self._private_client.query_POST_orders(**params)
    return fut

  def _async_cancel_order(self, order):
    assert order.internal.exchange_order_id is not None, order
    fut = self._private_client.query_POST_orders_cancel(order.internal.exchange_order_id)
    return fut

  def _handle_submit_response(self, order, response):
    """
    {
      "status": 0,
      "data": "Yw4pvx-5t306IrwQ_nrT39keERTkbmGtFqQXmUeXtwg="
    }"""
    msg = json.loads(response.content)
    if msg['status'] != 0:
      self._order_manager.remove_order(order, ignore_error=True)
      self._logger.error('Order submit error! %s\n%s', msg, order)
    else:
      exchange_order_id = msg['data']
      prev_order = self._order_manager.get(exchange_order_id=exchange_order_id)
      if prev_order is None:
        self._order_manager.set_order(order, exchange_order_id=exchange_order_id)
      else:
        self._logger.info('order id replaced: %s => %s', prev_order.order_id, order.order_id)
        self._order_manager.remove_order(order)
        self._order_manager.remove_order(prev_order)
        prev_order.order_id = order.order_id
        self._order_manager.add_order(prev_order)

  def _handle_cancel_response(self, order, response):
    """
    {"status": 0}
    """
    msg = json.loads(response.content)
    if msg['status'] == 0:
      # Cancel in pending, confirm it by query order status 5 seconds later.
      self._rest_queryer.query_once(self._private_client.query_GET_orders_by_order_id,
                                    order_id=order.internal.exchange_order_id,
                                    msg_type='order_info',
                                    delay=5)
    else:
      self._logger.error('Cancel order error: %s\n%s', msg, order)
      order.cancel_sent = False

  @exchange_message_handler('account_balance')
  def _handle_account_balance(self, exchange_message):
    update_msg = json.loads(exchange_message.data.content)
    assert update_msg['status'] == 0, update_msg
    self._og_info.update_balance(update_msg)
    self._og_logger.write_balance(self._og_info.balance_as_proto())

  def _update_order(self, update_msg):
    assert update_msg['status'] == 0, update_msg
    timestamp = get_timestamp()

    for order_info in to_list(update_msg['data']):
      exchange_order_id = order_info['id']
      order_state = order_info['state']
      order = self._order_manager.get(exchange_order_id=exchange_order_id)
      if order is None:
        if order_state in ('filled', 'canceled', 'partial_canceled'):
          # Ignore dead foreign orders.
          continue
        else:
          order_id = self._order_id_generator.gen_order_id(is_foreign=True)
          order = create_order_from_order_info(order_id, order_info)
          self._order_manager.add_order(order)

      if order_state in ('canceled', 'partial_canceled'):  # DEAD
        self._order_manager.set_order(order, cancel_confirmed=True)
      elif order_state == 'filled':  # DEAD
        self._order_manager.set_order(order, fully_filled=True)
      elif order_state == 'pending_cancel':  # dying
        if not order.cancel_sent:
          self._logger.info('Cancel not send, but order is in pending_cancel state. %s', order)
          order.cancel_sent = True
      elif order_state in ('submitted', 'partial_filled'):
        if self._order_manager.is_dead_order(order):
          self._logger.info('Order is already marked as dead, %s', order)
        else:
          if order.internal.exchange_order_id is None:
            order.internal.exchange_order_id = order_info['id']
          else:
            if order.internal.exchange_order_id != order_info['id']:
              self._logger.error('Inconsistent exchange_order_id, %s\n%s', order_info['id'], order)
          if order.cancel_sent and timestamp - order.internal.cancel_sent_time > 30e9:
            order.internal.cancel_sent = False
      else:
        self._logger.error('Unknown order status: %s', update_msg)

      last_fill = None
      qty_accum_fill = float(order_info['filled_amount'])
      if not math.isclose(qty_accum_fill, 0):
        price_avg_fill = float(order_info['executed_value']) / qty_accum_fill
        last_fill = order.internal.fill.update_by_accum_fill(price_avg_fill, qty_accum_fill)
      if last_fill is not None:
        self._publish_fill(last_fill.price_last_fill, last_fill.qty_last_fill, order)
        order.internal.fill.log_last_fill(order, self._logger)
        self._og_logger.gen_order_filled_event_and_log(order, timestamp)

  @exchange_message_handler('order_info')
  def _handle_order_info(self, exchange_message):
    update_msg = json.loads(exchange_message.data.content)
    self._update_order(update_msg)
    if not self.is_ready():
      if None not in set(self.og_info.get_balances().values()):
        self._og_ready.set_status(OrderGatewayStatus.READY)
